Skip to content

Refactor FastMCP to inherit from Provider#2901

Merged
jlowin merged 10 commits intomainfrom
refactor-provider-inheritance-v2
Jan 17, 2026
Merged

Refactor FastMCP to inherit from Provider#2901
jlowin merged 10 commits intomainfrom
refactor-provider-inheritance-v2

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Jan 17, 2026

FastMCP now inherits from Provider, establishing a cleaner class hierarchy. Previously these were parallel implementations; now FastMCP is-a Provider with aggregation capabilities on top.

What changed:

Provider provides the transform chain machinery - the _get_* and _list_* methods that wrap raw lookups with transforms. FastMCP overrides _get_* to aggregate from multiple sub-providers before applying its own transforms. This isn't pure inheritance (aggregation requires custom logic), but it shares the transform chain pattern.

Visibility is now consistently the last transform via a .transforms property:

@property
def transforms(self) -> list[Transform]:
    return [*self._transforms, self._visibility]

Applied last means visibility sees components after transforms modify them - so tags added by transforms work with enable/disable filtering.

Other fixes:

  • AuthMiddleware uses get_* (with component auth) instead of _get_* (without)
  • Removed dead code (get_component, redundant _is_component_enabled checks)
  • Replaced asserts with proper NotFoundError in component_service

FastMCP now properly inherits from Provider, eliminating ~200 lines of
duplicated _source_* methods. Key changes:

- get_tool/resource/prompt return None instead of raising NotFoundError
- Visibility filter separated from transforms (applied last)
- Nested server middleware runs on both list and execution operations
- Resource auth failure doesn't fall back to templates
- AggregateProvider kept as user-facing utility class
Resolved conflicts to combine Provider inheritance refactor with versioning feature:
- FastMCP now properly inherits from Provider
- get_tool/resource/template/prompt return None instead of raising NotFoundError
- Deduplication uses version_sort_key to keep highest version per name/URI
- _source_* methods eliminated in favor of inherited _* methods
- Override _get_tool/resource/template/prompt in FastMCP to apply
  server transforms over provider aggregation
- Update FastMCPProvider get_* methods to check for None (not
  NotFoundError since get_* now returns None)
- Update versioning tests to expect None instead of NotFoundError
  when requesting filtered/nonexistent versions
- Add None checks in auth and tool transform tests
- Add assertions in component_service.py for None returns
- Add type ignore comments for max() with version_sort_key
…eritance-v2

# Conflicts:
#	src/fastmcp/server/server.py
Replace _get_all_transforms() with a .transforms property that returns
[*self._transforms, self._visibility]. This cleanly separates user transforms
from visibility filtering while keeping visibility applied last (outermost).

Also:
- AuthMiddleware now uses get_* instead of _get_* for proper component auth
- Remove redundant _is_component_enabled checks (visibility is a transform)
- Delete dead code (get_component method)
- Add versions field to FastMCPMeta
- Replace asserts with NotFoundError in component_service
@marvin-context-protocol marvin-context-protocol Bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. provider Related to the FastMCP Provider class auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. labels Jan 17, 2026
@jlowin jlowin changed the title Simplify Provider transform architecture Refactor FastMCP to inherit from Provider Jan 17, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ade5a4838

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 67 to 70
def __init__(self) -> None:
# Visibility is the first (innermost) transform - closest to the base provider
self._visibility = Visibility()
self._transforms: list[Transform] = [self._visibility]
self._transforms: list[Transform] = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply visibility when FastMCPProvider registers tasks

Now that _transforms no longer includes Visibility, any code that still iterates self._transforms directly will skip visibility filtering. FastMCPProvider.get_tasks() still wraps only self._transforms, so provider-level enable/disable (or allowlist) won’t be applied to task registration for mounted servers, and disabled components can still be registered with Docket. This regression is introduced by separating visibility from _transforms; update FastMCPProvider.get_tasks() to iterate self.transforms instead (see src/fastmcp/server/providers/fastmcp_provider.py).

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 17, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR refactors the server architecture to consolidate provider aggregation, revise error handling patterns, and introduce version-based component lookups. Key changes include: FastMCP now inherits from Provider with centralized list/get operations that aggregate results from sub-providers; visibility transforms are reordered to apply as the outermost layer; NotFoundError imports are replaced with explicit None checks and AuthorizationError raises; component lookups use VersionSpec for versioning; FastMCPMeta includes a new versions field; and middleware handling is unified through aggregated paths across multiple provider implementations.

Possibly related PRs

  • #2897: Implements version parameter plumbing through server and provider lookups via VersionSpec
  • #2622: Modifies provider abstraction and changes get_* method semantics to return None instead of raising NotFoundError
  • #2894: Updates component_service enable/disable resolution to handle multi-version keys and return the highest version
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Refactor FastMCP to inherit from Provider' clearly summarizes the main structural change - FastMCP's inheritance relationship now extends Provider.
Description check ✅ Passed The description provides a clear explanation of the changes, including what changed (inheritance structure, transform chain machinery, visibility ordering), why it matters (cleaner class hierarchy, consistency), and other fixes (AuthMiddleware, dead code removal).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/fastmcp/server/providers/__init__.py (1)

30-48: Confirm AggregateProvider belongs in top-level exports.

If it’s a specialized utility, consider keeping it in the submodule to limit the public surface; otherwise this is fine. As per coding guidelines, keep init.py re-exports minimal.

src/fastmcp/server/server.py (1)

1104-1107: Silence unused context args in middleware call_next lambdas.
Rename the unused parameter (e.g., _context) to satisfy lint and make intent explicit; apply similarly to the other call_next lambdas.

♻️ Example tweak
-                return await self._run_middleware(
-                    context=mw_context,
-                    call_next=lambda context: self.get_tools(run_middleware=False),
-                )
+                return await self._run_middleware(
+                    context=mw_context,
+                    call_next=lambda _context: self.get_tools(run_middleware=False),
+                )

Also applies to: 1222-1225, 1344-1349, 1466-1469, 2005-2008, 2031-2034

Comment thread src/fastmcp/server/providers/fastmcp_provider.py
Comment thread src/fastmcp/server/server.py Outdated
- get_*() now does aggregation + component auth (raises AuthorizationError)
- Deleted _get_*() overrides - inherited from Provider applies transforms
- Simplified AuthMiddleware to global auth only
- Changed version params to VersionSpec | None (not str | None)
- Updated tests to use _get_*() where visibility filtering is expected
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Jan 17, 2026

Test Failure Analysis

Summary: Multiple versioning tests are failing because FastMCP-level transforms (like VersionFilter) are not being applied when calling get_tool() with a specific version.

Root Cause: The PR refactors FastMCP to inherit from Provider, but FastMCP.get_tool() doesn't apply FastMCP's own transforms after aggregating from sub-providers.

The transform chain flow:

  1. Provider._get_tool() builds a transform chain that wraps calls to self.get_tool()
  2. FastMCP.get_tool() overrides the base to aggregate from sub-providers using p._get_tool()
  3. Each sub-provider correctly applies its own transforms via p._get_tool()
  4. But FastMCP.get_tool() doesn't apply FastMCP's own transforms to the aggregated result
  5. When tests call mcp.get_tool("add", VersionSpec(eq="1.0")) directly, they bypass the transform chain

The issue is that FastMCP.get_tool() is meant to be the "raw" base implementation (like Provider.get_tool()), but FastMCP has its own transforms that should wrap this raw aggregation. The inherited Provider._get_tool() doesn't help because:

  • _get_tool() builds a chain: transforms → self.get_tool()
  • self.get_tool() is FastMCP.get_tool() which already does aggregation
  • But aggregation calls p._get_tool() on sub-providers, which already includes their transforms
  • So FastMCP transforms should wrap the final aggregated result, not each sub-provider call

Failing tests (all expect None but get the filtered-out tool):

  • test_version_gte_filters_low_versions: VersionFilter(version_gte="2.0") should filter out v1.0
  • test_version_range: VersionFilter(version_gte="2.0", version_lt="3.0") should filter v1.0 and v3.0
  • test_get_tool_respects_filter: Version filtering on get_tool
  • Mounted server version filtering tests

Suggested Solution:

The issue is subtle: FastMCP inherits from Provider and has transforms, but its get_tool() override doesn't go through the inherited _get_tool() transform wrapper. The inherited _get_tool() builds a chain that calls self.get_tool() as the base, but since FastMCP.get_tool() aggregates from providers (each already transformed), the FastMCP transforms are never applied.

Fix approach: FastMCP should override both get_tool() (raw aggregation) AND list_tools() etc, but these should be called through _get_tool()/_list_tools() which apply FastMCP's transforms. Currently, external code and tests call mcp.get_tool() directly.

Two options:

  1. Tests should call _get_tool() - Breaking change, tests need updating
  2. FastMCP.get_tool() should apply transforms - Add transform application after aggregation

Looking at the code flow, I believe Option 2 is correct: when FastMCP.get_tool() is called (even directly), it should apply FastMCP's own transforms. The aggregation from sub-providers is the "raw" lookup, and then FastMCP transforms should filter the result.

Detailed Analysis

Error from logs:

E       AssertionError: assert FunctionTool(name='add', version='1.0', ...) is None
tests/server/test_versioning.py:492: AssertionError

Test code change in PR (lines 486-492):

# Can request specific versions in range  
tool_v2 = await mcp.get_tool("add", VersionSpec(eq="2.0"))
assert tool_v2 is not None
assert tool_v2.version == "2.0"

# Cannot request version outside range - returns None
assert await mcp.get_tool("add", VersionSpec(eq="1.0")) is None

The test adds VersionFilter(version_gte="2.0") to the server, then expects get_tool("add", VersionSpec(eq="1.0")) to return None. However, it returns the v1.0 tool because the VersionFilter transform is never applied to the aggregated result.

Current FastMCP.get_tool() flow:

  1. Calls p._get_tool(name, version) on each provider (sub-providers apply their transforms)
  2. Collects results and picks highest version
  3. Does auth check
  4. Returns tool
  5. ❌ Never applies FastMCP's own transforms (like VersionFilter)
Related Files
  • src/fastmcp/server/server.py lines 1248-1295: FastMCP.get_tool() - needs to apply self.transforms
  • src/fastmcp/server/providers/base.py lines 118-138: Provider._get_tool() - builds transform chain
  • tests/server/test_versioning.py lines 460-496: Failing version filter tests

This is an automated analysis from the FastMCP test failure triage bot. Edit 1: Clarified root cause and transform chain flow.

FastMCPProvider now calls _get_* methods instead of get_* to ensure
nested server transforms are applied during lookups. Also converts
string versions to VersionSpec in MCP handlers.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/fastmcp/server/server.py (1)

906-962: Filter task‑eligible components before returning from get_tasks.

get_tasks() currently returns all transformed components. When a nested FastMCP is mounted, this can register non‑task components with Docket even if their task_config forbids tasks. The base Provider implementation filters by supports_tasks(), so aligning here prevents accidental registration.

🛠️ Proposed fix
-        return [
-            *transformed_tools,
-            *transformed_resources,
-            *transformed_templates,
-            *transformed_prompts,
-        ]
+        return [
+            c
+            for c in [
+                *transformed_tools,
+                *transformed_resources,
+                *transformed_templates,
+                *transformed_prompts,
+            ]
+            if c.task_config.supports_tasks()
+        ]

- Filter task-eligible components in FastMCPProvider.get_tasks()
- Catch AuthorizationError in get_* methods and return None for consistency
- Remove AggregateProvider from top-level exports
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/fastmcp/server/server.py (1)

1094-1126: Variable shadowing: ctx reused for different types.

The variable ctx from the async context manager (line 1094) is shadowed by ctx = AuthContext(...) (line 1118). While the outer context isn't used after this point in this code path, this shadowing pattern is repeated across get_resources, get_resource_templates, and get_prompts, making the code harder to follow and potentially error-prone during future modifications.

♻️ Suggested refactor to avoid shadowing
         async with fastmcp.server.context.Context(fastmcp=self) as ctx:
             if run_middleware:
                 mw_context = MiddlewareContext(
                     message=mcp.types.ListToolsRequest(method="tools/list"),
                     source="client",
                     type="request",
                     method="tools/list",
                     fastmcp_context=ctx,
                 )
                 return await self._run_middleware(
                     context=mw_context,
                     call_next=lambda context: self.get_tools(run_middleware=False),
                 )

             # Query through full transform chain (provider transforms + server transforms + visibility)
             tools = await self._list_tools()

             # Get auth context (skip_auth=True for STDIO which has no auth concept)
             skip_auth, token = _get_auth_context()

             # Filter by auth
             authorized: list[Tool] = []
             for tool in tools:
                 if not skip_auth and tool.auth is not None:
-                    ctx = AuthContext(token=token, component=tool)
+                    auth_ctx = AuthContext(token=token, component=tool)
                     try:
-                        if not run_auth_checks(tool.auth, ctx):
+                        if not run_auth_checks(tool.auth, auth_ctx):
                             continue
                     except AuthorizationError:
                         continue
                 authorized.append(tool)

Apply similar changes to get_resources (line 1216), get_resource_templates (line 1317), and get_prompts (line 1414).

Comment on lines +1192 to 1204
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
if run_middleware:
mw_context = MiddlewareContext(
message={}, # List resources doesn't have parameters
message={},
source="client",
type="request",
method="resources/list",
fastmcp_context=fastmcp_ctx,
fastmcp_context=ctx,
)
return list(
await self._run_middleware(
context=mw_context,
call_next=lambda context: self.get_resources(
run_middleware=False
),
)
return await self._run_middleware(
context=mw_context,
call_next=lambda context: self.get_resources(run_middleware=False),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent message types in MiddlewareContext.

The get_resources, get_resource_templates, and get_prompts methods use message={} (lines 1195, 1294, 1393) while get_tools uses a proper mcp.types.ListToolsRequest (line 1097). If middleware inspects the message type or contents, this inconsistency could lead to unexpected behavior.

🔧 Suggested fix for consistency

Consider using the appropriate MCP request types:

             mw_context = MiddlewareContext(
-                message={},
+                message=mcp.types.ListResourcesRequest(method="resources/list"),
                 source="client",
                 type="request",
                 method="resources/list",
                 fastmcp_context=ctx,
             )

Apply similar changes:

  • get_resource_templates: Use appropriate request type for resources/templates/list
  • get_prompts: Use mcp.types.ListPromptsRequest(method="prompts/list")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
if run_middleware:
mw_context = MiddlewareContext(
message={}, # List resources doesn't have parameters
message={},
source="client",
type="request",
method="resources/list",
fastmcp_context=fastmcp_ctx,
fastmcp_context=ctx,
)
return list(
await self._run_middleware(
context=mw_context,
call_next=lambda context: self.get_resources(
run_middleware=False
),
)
return await self._run_middleware(
context=mw_context,
call_next=lambda context: self.get_resources(run_middleware=False),
)
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
if run_middleware:
mw_context = MiddlewareContext(
message=mcp.types.ListResourcesRequest(method="resources/list"),
source="client",
type="request",
method="resources/list",
fastmcp_context=ctx,
)
return await self._run_middleware(
context=mw_context,
call_next=lambda context: self.get_resources(run_middleware=False),
)
🧰 Tools
🪛 Ruff (0.14.13)

1203-1203: Unused lambda argument: context

(ARG005)

@jlowin jlowin merged commit 5d12afd into main Jan 17, 2026
11 checks passed
@jlowin jlowin deleted the refactor-provider-inheritance-v2 branch January 17, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. enhancement Improvement to existing functionality. For issues and smaller PR improvements. provider Related to the FastMCP Provider class server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant